import static org.junit.Assert.assertTrue;
import initPackage.AddCandidate;
import initPackage.AddVoter;

import java.math.BigInteger;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;

import org.junit.Before;
import org.junit.Test;

import registration.CheckUserData;
import registration.PersonalData;
import shamir.shamirGenerator;
import shamir.shamirResolver;
import sun.misc.BASE64Encoder;
import util.Utils;

import com.google.common.collect.Lists;

import data.Blind;
import data.CardsManager;
import data.HashedCard;
import data.VotesCounter;
import data.VotingCard;
import data.VotingSet;
import database.ConnectionHandler;

public class WholeVotingTest
{
    volatile Object lock = new Object();
    ConnectionHandler connHandler = new ConnectionHandler();
    volatile Object firstVoteLock = new Object();
    String firstHash;

    @Before
    public void clearDatabase()
    {
        String[] sql = { "DELETE FROM CANDIDATE", "DELETE FROM VOTER", "DELETE FROM \"codedVotes\"", "DELETE FROM VOTES" };
        Connection c = connHandler.getConnection();
        for( String s: sql )
        {
            try
            {
                Statement st = c.createStatement();
                st.execute( s );
            }
            catch( Exception e )
            {
                throw new RuntimeException( e );
            }
        }
    }

    private void initializeVoters()
    {
        String[] voters = { "Voter1", "Voter2", "Voter3" };
        int i = 0;
        for( String s: voters )
        {
            AddVoter.addVoter( s, s, new String( new char[ 11 ] ).replace( "\0", String.valueOf( i++ ) ), s, false );
        }
    }

    private void initializeCandidates()
    {
        AddCandidate.addCandidate( "Jerzy", "Brzeczyszczykiewicz" );
        AddCandidate.addCandidate( "Marian", "Lubiegowojwski" );
        AddCandidate.addCandidate( "Ktos", "Ktos" );
        AddCandidate.addCandidate( "Niewiem", "Jakotaki" );
    }

    String path = "d:/shamir";

    private void initShamir()
    {
        shamirGenerator sG = new shamirGenerator( path + "priv", path + "pub.txt" );
        sG.generate();
        try
        {
            publicVotesKey = (RSAPublicKey)Utils.readPublicKey( path + "pub.txt" );
        }
        catch( Exception e )
        {
            System.out.println( "UNABLE TO READ SHAMIR KEY" );
            throw new RuntimeException( e );
        }
    }

    private void reconstructShamir()
    {
        String[] tab = { path + "priv1.txt", path + "priv2.txt", path + "priv4.txt" };
        shamirResolver sR = new shamirResolver( tab, path + "pub.txt" );
        privateVotesKey = (RSAPrivateKey)sR.joinShamir();
    }

    private void checkUserData()
    {
        // simulate creating certificates
        try
        {
            PersonalData data1 = new PersonalData( "Voter1", "Voter1", "00000000000", null, null, "Voter1" );
            assertTrue( CheckUserData.check( data1 ) );
            assertTrue( CheckUserData.check( data1 ) == false );// cannot get
                                                                // second
            // certificate
            PersonalData falseData = new PersonalData( "Voter2", "Voter2", "11111111111", null, null, "BadPassword" );
            assertTrue( CheckUserData.check( falseData ) == false );
            PersonalData trueData = new PersonalData( "Voter2", "Voter2", "11111111111", null, null, "Voter2" );
            assertTrue( CheckUserData.check( trueData ) );
        }
        catch( Exception e )
        {
            throw new RuntimeException( e );
        }
    }

    private void castVote( String voteFor, String voterPesel ) throws Exception
    {
        // ssl connection...
        // server creates voting card
        VotingCard card = new VotingCard( false );
        Statement st = connHandler.getConnection().createStatement();
        ResultSet rs = st.executeQuery( "SELECT * from candidate" );
        LinkedList<String> candidates = Lists.newLinkedList();
        while( rs.next() )
        {
            String name = rs.getString( 1 ) + " " + rs.getString( 2 ) + " " + rs.getString( 3 );
            card.addCandidate( name );
            candidates.add( name );
        }
        // client connection
        // server sends card to him
        // CLIENT PART
        String receivedCard = card.toString();
        CardsManager manager = new CardsManager( receivedCard );
        List<VotingSet> sets = manager.exportSets();
        // blind all sets
        List<BigInteger> tab = Lists.newLinkedList(); // table of blinding
                                                      // values
        List<String> blindedSets = Lists.newLinkedList();
        Blind b = new Blind();
        for( VotingSet set: sets )
            blindedSets.add( b.doBlind( set, pubKey, tab ) );
        // client sends all blindedsets
        // for(String s:blindedsets
        // send(s);
        // SERVER PART
        // send random number % 100 - the number of card which will be signed
        int randomServerNumber = new Random().nextInt( 99 ) + 1;
        // CLIENT PART
        StringBuilder response = new StringBuilder();
        int setsNumber = 100; // actually hardcoded in cards manager
        for( int i = 0; i < setsNumber; i++ )
        {
            if( i == randomServerNumber )
                continue;
            response.append( tab.get( i ) + "\r\n" );
        }
        String sentResponse = response.toString();
        // SERVER PART
        // server unblinds all sets and check if they are correct
        String[] serverBlindParameters = sentResponse.split( "\r\n" );
        for( int i = 0; i < setsNumber; i++ )
        {
            if( i == randomServerNumber )
                continue;
            int parameterNumber = i;
            if( i > randomServerNumber )
                parameterNumber--;
            BigInteger blindParameter = new BigInteger( serverBlindParameters[ parameterNumber ] );
            assertTrue( tab.get( i ).equals( blindParameter ) );
            String blindSet = blindedSets.get( i );
            blindSet = b.sign( blindSet, privKey );
            blindSet = b.unblind( blindSet, pubKey, blindParameter );
            VotingSet unblindedSet = b.unsignSet( blindSet, pubKey );
            assertTrue( unblindedSet.isValid( candidates ) );
            // SET IS OK
        }
        // ALL SETS ARE OK
        String selectedSet = blindedSets.get( randomServerNumber );
        String selectedSignedSet = b.sign( selectedSet, privKey );
        // server sets that candidate has already voted
        // server sends selected signed card
        // CLIENT PART
        selectedSignedSet = b.unblind( selectedSignedSet, pubKey, tab.get( randomServerNumber ) );
        // vote for is selected in the form
        VotingSet normalSelectedSet = sets.get( randomServerNumber );
        int candidateNumber = normalSelectedSet.getVoteNumber( voteFor );
        String selectedVote = b.pickCard( selectedSignedSet, candidateNumber );
        String voteToSend = b.SecHash( selectedVote, publicVotesKey );
        // sends VOTE
        // server tests whether voter hasnt already voted
        synchronized( lock )
        {
            try
            {
                st = connHandler.getConnection().createStatement();
                rs = st.executeQuery( "SELECT * from voter where pesel='" + voterPesel + "' and hascard='t'" );
                if( rs.next() )
                {
                    // CHEATER!!!!!!!
                    // System.out.println( "You've already voted!" );
                    return;
                }
                st = connHandler.getConnection().createStatement();
                st.executeUpdate( "UPDATE voter set hascard='t' where pesel='" + voterPesel + "'" );
            }
            catch( Exception e )
            {
                throw new RuntimeException( e );
            }
        }
        // server writes it down to database and sends back digest
        MessageDigest md5 = null;
        try
        {
            md5 = MessageDigest.getInstance( "MD5" );
        }
        catch( Exception e )
        {
            throw new RuntimeException( e );
        }
        BASE64Encoder encoder = new BASE64Encoder();
        String voteDigest = encoder.encode( md5.digest( voteToSend.getBytes() ) );
        synchronized( firstVoteLock )
        {
            HashedCard hasheddatabasecard = new HashedCard( voteToSend );
            if( firstHash == null )
                firstHash = voteDigest;
        }
        // lets imagine, that all the voters tried to send the same vote three
        // times
        for( int i = 0; i < 20; i++ )
        {
            HashedCard hasheddatarepeat = new HashedCard( voteToSend );
        }
        // and our voter is so naughty and evil that he tries to send the same
        // card second time choosing another candidate!
        candidateNumber = normalSelectedSet.getVoteNumber( "3 Ktos Ktos" );
        selectedVote = b.pickCard( selectedSignedSet, candidateNumber );
        voteToSend = b.SecHash( selectedVote, publicVotesKey );
        HashedCard hasheddatarepeat2222 = new HashedCard( voteToSend );
    }

    private void publishResults()
    {
        try
        {
            VotesCounter counter = new VotesCounter( pubKey, privateVotesKey );
            Map<String, Integer> results = counter.count();
            int i = 0;
            for( Entry<String, Integer> e: results.entrySet() )
            {
                i++;
                switch (i)
                {
                    case 1:
                        assertTrue( 2 == e.getValue().intValue() );
                        break;
                    case 2:
                        assertTrue( 1 == e.getValue().intValue() );
                        break;
                    default:
                        assertTrue( 0 == e.getValue().intValue() );
                }
            }
            System.out.println( "------------" );
            System.out.println( counter.votesAsString() );
            System.out.println( "-----------" );
            System.out.println( counter.showAllValidVotesHashes() );
            System.out.println( "------------" );
            assertTrue( counter.wasVoteCounted( firstHash ) );
            MessageDigest md5 = MessageDigest.getInstance( "MD5" );
            BASE64Encoder encoder = new BASE64Encoder();
            assertTrue( counter.wasVoteCounted( encoder.encode( md5.digest( "hash czegos tam ?".getBytes() ) ) ) == false );
        }
        catch( Exception e )
        {
            System.out.println( e );
            throw new RuntimeException( e );
        }
    }

    @Test
    public void test() throws Exception
    {
        try
        {
            clearDatabase();
            initShamir();
            // TEMP2 GENERATE KEYS
            KeyPairGenerator keyGen = null;
            try
            {
                keyGen = KeyPairGenerator.getInstance( "RSA" );
            }
            catch( Exception e )
            {
                System.out.println( e );
                throw new RuntimeException( e );
            }
            keyGen.initialize( 1024, new SecureRandom() );
            KeyPair keypair = keyGen.genKeyPair();
            privKey = (RSAPrivateKey)keypair.getPrivate();
            pubKey = (RSAPublicKey)keypair.getPublic();
            // END OF TEMP2
            initializeVoters();
            initializeCandidates();
            checkUserData();
            List<Runnable> runnables = Lists.newLinkedList();
            int numberOfThreads = 4;
            for( int i = 0; i < numberOfThreads; i++ )
                runnables.add( new VotersSimulator() );
            List<Thread> threads = Lists.newLinkedList();
            for( int i = 0; i < numberOfThreads; i++ )
                threads.add( new Thread( runnables.get( i ) ) );
            for( int i = 0; i < numberOfThreads; i++ )
                threads.get( i ).start();
            for( int i = 0; i < numberOfThreads; i++ )
                threads.get( i ).join();
            reconstructShamir();
            publishResults();
        }
        catch( Exception e )
        {
            e.printStackTrace();
            throw new RuntimeException( e );
        }
    }

    class VotersSimulator implements Runnable
    {
        @Override
        public void run()
        {
            try
            {
                castVote( "1 Jerzy Brzeczyszczykiewicz", "00000000000" );
                castVote( "1 Jerzy Brzeczyszczykiewicz", "00000000000" );
                castVote( "1 Jerzy Brzeczyszczykiewicz", "00000000000" );
                castVote( "1 Jerzy Brzeczyszczykiewicz", "11111111111" );
                castVote( "3 Ktos Ktos", "22222222222" );
                castVote( "3 Ktos Ktos", "11111111111" );
            }
            catch( Exception e )
            {
                e.printStackTrace();
                throw new RuntimeException( e );
            }
        }
    }

    RSAPrivateKey privKey;
    RSAPublicKey pubKey;
    RSAPrivateKey privateVotesKey;
    RSAPublicKey publicVotesKey;
}
